package com.darcytech.debezium.common.secret.vault;

import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.bettercloud.vault.Vault;
import com.bettercloud.vault.VaultConfig;
import com.bettercloud.vault.VaultException;
import com.bettercloud.vault.response.AuthResponse;

public class VaultClient {

    private static final Logger LOGGER = LoggerFactory.getLogger(VaultClient.class);

    private static final Integer READ_TIMEOUT = 5;

    private static final Integer OPEN_TIMEOUT = 30;

    private static final AtomicInteger threadNum = new AtomicInteger();

    private final String address;

    private final String roleId;

    private final String secretId;

    private final String basePath;

    private final AuthTokenProvider authTokenProvider;

    public VaultClient(final String address, final String roleId, final String secretId, final String basePath) {
        this.address = address;
        this.roleId = roleId;
        this.secretId = secretId;
        this.basePath = basePath;
        authTokenProvider = VaultClient.this.new AuthTokenProvider();
        new Thread(authTokenProvider, "vaultClient-authTokenProvider-thread-" + threadNum.getAndIncrement()).start();
    }

    public String getValue(String path, String property) throws VaultException {
        Map<String, String> data = getValue(path);
        return data.get(property);
    }

    public Map<String, String> getValue(String path) throws VaultException {
        String basePath = this.basePath;
        if (basePath.endsWith("/")) {
            basePath = basePath.substring(0, basePath.length() - 1);
        }
        String fullPath = basePath + "/" + path;

        String authToken = authTokenProvider.getAuthToken();
        if (authToken == null) {
            throw new VaultException("vault login failed");
        }
        final Vault vault = getVault(address, authTokenProvider.getAuthToken());
        return vault.logical().read(fullPath).getData();
    }

    private final class AuthTokenProvider implements Runnable {

        private volatile String authToken;

        private String getAuthToken() {
            if (this.authToken != null) {
                return this.authToken;
            }
            synchronized (VaultClient.this) {
                if (this.authToken != null) {
                    return this.authToken;
                }
                try {
                    VaultClient.this.wait();
                } catch (InterruptedException ignore) {
                }
                return this.authToken;
            }
        }

        @Override
        public void run() {
            String roleId = VaultClient.this.roleId;
            String secretId = VaultClient.this.secretId;
            String address = VaultClient.this.address;
            Vault vault;
            try {
                vault = VaultClient.this.getVault(address);
            } catch (VaultException e) {
                throw new RuntimeException("vault address in null");
            }

            maintainToken(vault, roleId, secretId, address);
        }

        private AuthResponse renewAuth(Vault renewVault) throws VaultException {
            return renewVault.auth().renewSelf(240);
        }

        private AuthResponse createAuth(Vault vault, String roleId, String secretId) throws VaultException {
            return vault.auth().loginByAppRole(roleId, secretId);
        }

        private void maintainToken(Vault vault, String roleId, String secretId, String address) {

            try {
                AuthResponse auth = createAuth(vault, roleId, secretId);
                synchronized (VaultClient.this) {
                    this.authToken = auth.getAuthClientToken();
                    VaultClient.this.notifyAll();
                }

                final Vault renewVault = getVault(address, this.authToken);
                while (true) {
                    try {

                        long remainMs = auth.getAuthLeaseDuration() * 1000;
                        long advanceMs = 30 * 1000;

                        long sleepMs = remainMs - advanceMs;
                        if (sleepMs <= 0) {
                            throw new VaultException("authToken expired");
                        }
                        Thread.sleep(sleepMs);
                    } catch (InterruptedException ignore) {
                    }

                    auth = renewAuth(renewVault);
                }
            } catch (VaultException e) {
                LOGGER.error("vault error message: {}" + e.getMessage());
                maintainToken(vault, roleId, address, secretId);
            }

        }

    }

    private Vault getVault(final String address) throws VaultException {
        final VaultConfig config = new VaultConfig()
                .address(address)
                .openTimeout(READ_TIMEOUT)
                .readTimeout(OPEN_TIMEOUT)
                .build();
        return new Vault(config);
    }

    private Vault getVault(final String address, final String clientToken) throws VaultException {
        final VaultConfig config = new VaultConfig()
                .address(address)
                .token(clientToken)
                .openTimeout(READ_TIMEOUT)
                .readTimeout(OPEN_TIMEOUT)
                .build();
        return new Vault(config);
    }

}
